package yolo

import (
	"appengine"
	"fmt"
	"net/http"
	"net/url"
	"regexp"
	"strconv"
	"strings"
	"time"
)

const (
	rietveldTimeoutLayout = "2006-01-02 15:04:05"
	rietveldExcludeUser   = "commit-bot@chromium.org"
	rietveldUpdateFetchMaxIssues = 20
)

func init() {
	RegisterPoller(SiteRietveldChromium, NewRietveldPoller)
	RegisterPoller(SiteRietveldAppspot, NewRietveldPoller)

	RegisterIssueHandler(SiteRietveldChromium, NewRietveldIssueHandler)
	RegisterIssueHandler(SiteRietveldAppspot, NewRietveldIssueHandler)
}

// JSON schema ------------------------------------------------------------
type RietveldSearchResults struct {
	Cursor string
	Results []int64
}

type RietveldMessageData struct {
	Date string
	Text string
	Sender string
	Recipients []string
	Approval bool
}

type RietveldIssueData struct {
	Issue int64
	Subject string
	Description string
	Created string
	Cc []string
	Reviewers []string
	Messages []RietveldMessageData
	Owner_email string
	Patchsets []int
	Modified string
	Private bool
	Base_url string
	Closed bool
	Owner string
	Commit bool
}

type RietveldFileData struct {
	Id int
	Status string
	No_base_file bool
	Is_binary bool
	Num_chunks int
	Property_changes string
	Num_added int
	Num_removed int
}

type RietveldPatchsetData struct {
	Files map[string]RietveldFileData
	Created string
	Num_comments int
	Owner_email string
	Modified string
	Owner string
	Patchset int
	Issue int64
}

func getRietveldJSONIssueURL(siteURL, issue string, get_messages bool) string {
	issue_url := fmt.Sprintf("http://%s/api/%s", siteURL, issue)
	if get_messages {
		issue_url += "?messages=true"
	}
	return issue_url
}

func getRietveldJSONPatchsetURL(siteURL, issueId string, patchId int) string {
	return fmt.Sprintf("http://%s/api/%s/%v", siteURL, issueId, patchId)
}

var (
	rietveldBugRegexp     = regexp.MustCompile("\nBUG=([0-9, ]+)")
	rietveldCommitRegexp  = regexp.MustCompile("\nCommitted: [a-z/:&=?.]+=([0-9]+)")
	rietveldCommitURLRegexp  = regexp.MustCompile("\nCommitted: ([a-z/:&=?.]+=[0-9]+)")
)

func parseRietveldIssueData(c appengine.Context, siteURL, issueId string, data *RietveldIssueData) *Issue {
	issue := new(Issue)

	description := strings.Replace(data.Description, "\r\n", "\n", -1)
	issue.IssueId = issueId
	issue.OwnerEmail = sanitizeEmail(data.Owner_email)
	issue.Subject = data.Subject
	issue.Description = makeSnippet(description, issueMaxDescriptionLength, "...")
	issue.Created, _ = stringToDatastoreTime(data.Created)
	issue.Modified, _ = stringToDatastoreTime(data.Modified)
	issue.Closed = data.Closed
	issue.BaseURL = data.Base_url
	issue.SiteURL = siteURL

	if m := rietveldBugRegexp.FindStringSubmatch(description); m != nil {
		bugs := strings.Split(m[1], ",")
		issue.Bugs = make([]int64, 0, len(bugs))
		for _, v := range bugs {
			bug, err := strconv.ParseInt(strings.Trim(v, " "), 10, 0)
			if err == nil && bug != 0 {
				issue.Bugs = append(issue.Bugs, bug)
			}
		}
	}

	if m := rietveldCommitRegexp.FindStringSubmatch(description); m != nil {
		issue.Commit, _ = strconv.ParseInt(m[1], 10, 0)
	}
	return issue
}

func loadRietveldIssueFiles(c appengine.Context, siteURL, issueId string, data *RietveldIssueData) ([]string, error) {
	if len(data.Patchsets) > 0 {
		patchId := data.Patchsets[len(data.Patchsets) - 1]
		var patch RietveldPatchsetData
		patchURL := getRietveldJSONPatchsetURL(siteURL, issueId, patchId)
		err := loadJSONURL(c, patchURL, &patch)
		if err != nil {
			return []string{}, nil
		}
		files := make([]string, 0, len(patch.Files))
		for k, _ := range patch.Files {
			files = append(files, k)
		}
		return files, nil
	}
	return []string{}, nil
}

// Poller -----------------------------------------------------------------
type RietveldPoller struct {
	*PollContext
}

func NewRietveldPoller(pc *PollContext) Poller {
	poller := &RietveldPoller{pc}
	return poller
}

func (poller *RietveldPoller) Start(c appengine.Context) error {
	return poller.startFetchIssues(c)
}

func (poller *RietveldPoller) Delete(c appengine.Context) error {
	// Nothing to do.
	return nil
}

func (poller *RietveldPoller) RunTask(c appengine.Context, r *http.Request) error {
	issue := r.FormValue("issue")
	retry, _ := strconv.Atoi(r.FormValue("retry"))
	if issue == "" {
		return NewYoloError("RietveldPoller.RunTask(): issue needs to be specified")
	}
	return poller.fetchIssue(c, issue, retry)
}

func (poller* RietveldPoller) postFetchTask(c appengine.Context, issue string, retry int, delay time.Duration) error {
	return poller.PostTask(c, "", delay,
		map[string][]string{
		"issue": {issue},
		"retry": {strconv.FormatInt(int64(retry), 10)}})
}

func (poller *RietveldPoller) getJSONSearchURL() string {
	var result url.URL
	result.Scheme = "http"
	result.Host = poller.SiteURL
	result.Path = "/search"
	params := make(url.Values)
	for v, k := range defaultSearchParams {
		params.Add(v, k)
	}
	switch poller.Mode {
	case PollModeUpdate:
		if poller.ModifiedAfter.After(time.Unix(0, 0)) {
			s := poller.ModifiedAfter.Format(rietveldTimeoutLayout)
			params.Add("modified_after", s)
			params.Add("limit", fmt.Sprintf("%d", rietveldUpdateFetchMaxIssues))
		}
	case PollModeBackdate:
		params.Add("modified_before", poller.ModifiedBefore.Format(rietveldTimeoutLayout))
		params.Add("modified_after", poller.ModifiedAfter.Format(rietveldTimeoutLayout))
	}
	result.RawQuery = params.Encode()
	return result.String()
}

func (poller* RietveldPoller) startFetchIssues(c appengine.Context) error {
	requestURL := poller.getJSONSearchURL()

	var results RietveldSearchResults
	err := loadJSONURL(c, requestURL, &results)
	if err != nil {
		if err == YoloDeadlineError || err == YoloHTTPStatusError(500) {
			c.Warningf("Got deadline/HTTP status error. Post retrying...")
			// Possibly temporary.  Retry in 2 min.
			poller.PostNextPoll(c, 120 * 1000 * 1000, false)
			return err
		}
		c.Warningf("loadJSONURL %s failed: %s", requestURL, err.Error())
		return err
	}

	c.Debugf("queried url: %s", requestURL)
	c.Debugf("issues (%d): %v", len(results.Results), results.Results)

	for i, issue := range results.Results {
		delay := time.Duration((i / 20)) * 30 * time.Second
		err := poller.postFetchTask(c, strconv.FormatInt(int64(issue), 10), 0, delay)
		if err != nil {
			c.Errorf("%s: Post error: %s", currentLocation(), err.Error())
			return err
		}
	}

	return poller.PostNextPoll(c, poller.getNextPollDelay(len(results.Results)), true)
}

func (poller* RietveldPoller) getNextPollDelay(issues_to_process int) time.Duration {
	// Heuristics.
	switch poller.Mode {
	case PollModeUpdate:
		if issues_to_process == 0 {
			return 3 * time.Minute
		} else if issues_to_process < 3 {
			return 1 * time.Minute
		}
		return 10 * time.Second
	case PollModeBackdate:
		return time.Duration(max(10, issues_to_process)) * 50 * time.Millisecond
	}
	panic(fmt.Sprintf("Unknown poller mode: %v", poller.Mode))
	return 0
}

func (poller* RietveldPoller) fetchIssue(c appengine.Context, issueId string, retry int) error {
	var oldIssue Issue
	if err := GetIssueById(c, issueId, &oldIssue); err == nil {
		if poller.Mode == PollModeBackdate {
			c.Infof("### Issue %v already exists: skipping", issueId)
			poller.RecordProgress(c, 1, time.Unix(0, 0))
			return nil
		}
	}

	var data RietveldIssueData
	requestURL := getRietveldJSONIssueURL(poller.SiteURL, issueId, true)
	err := loadJSONURL(c, requestURL, &data)
	if err != nil {
		if retry < 2 && (err == YoloDeadlineError || err == YoloHTTPStatusError(500)) {
			return poller.postFetchTask(c, issueId, retry + 1, 0)
		}
		return err
	}

	issue := parseRietveldIssueData(c, poller.SiteURL, issueId, &data)

	c.Debugf("queried url: %s", requestURL)
	c.Debugf("* issue %v: %s created by %s, created %s", issue.IssueId, issue.Subject, issue.OwnerEmail, issue.Created.Format(rietveldTimeoutLayout))

	poller.RecordProgress(c, 1, issue.Modified)

	err = issue.Store(c)
	if err != nil {
		if err.Error() == "TIMEOUT" && retry < 2 {
			return poller.postFetchTask(c, issueId, retry + 1, 0)
		}
		c.Errorf("%s: Storing issue %v failed: %s", currentLocation(), issueId, err.Error())
		return err
	}

	files, _ := loadRietveldIssueFiles(c, issue.SiteURL, issue.IssueId, &data)

	activities := poller.parseActivities(c, issue, &oldIssue, &data, files)
	err = RecordActivityDataMulti(c, activities)
	if err != nil {
		c.Warningf("%s: Recording activity failed: %s", currentLocation(), err.Error());
	}

	return err
}

func (poller* RietveldPoller) parseActivities(c appengine.Context, issue* Issue, oldIssue* Issue, data* RietveldIssueData, files []string) (activities []*ActivityData) {
	newActivityData := func() *ActivityData {
		a := new(ActivityData)
		a.Issue = issue.Id()
		a.IssueOwner = issue.OwnerEmail
		a.SiteURL = issue.SiteURL
		if len(files) <= maxActivityFiles {
			// Record files if # of files is less than a certain threshold.
			// If there're too many files there's simply no point to record the
			// activity on the files.
			a.Files = files
		}
		return a
	}

	lastActivity := time.Unix(0, 0)
	for i, msg := range data.Messages {
		if msg.Sender == rietveldExcludeUser {
			continue
		}

		a := newActivityData()
		a.User = msg.Sender
		a.Recipients = msg.Recipients
		a.Snippet = makeSnippet(msg.Text, maxActivitySnippetLength, "...")
		a.Time, _ = stringToDatastoreTime(msg.Date)
		a.CommentId = i + 1
		a.URL = getRietveldMessageURL(issue, a.CommentId)
		if msg.Approval {
			a.Type = ActivityApprove
		} else {
			a.Type = ActivityComment
		}
		activities = append(activities, a)
		lastActivity = a.Time
	}

	if issue.Commit > 0 && oldIssue.Commit == 0 {
		a := newActivityData()
		// Heuristics.  Issue might have been modified for maintenance
		// reasons (i.e. adding approval flag etc)
		a.Time = issue.Modified
		if issue.Modified.Sub(lastActivity) > (30 * 24 * time.Hour) {
			a.Time = lastActivity.Add(600 * time.Millisecond)
		}
		a.Type = ActivityCommit
		a.User = issue.OwnerEmail
		a.Recipients = data.Reviewers
		a.URL = getRietveldCommitURL(issue)
		a.Snippet = fmt.Sprintf("Committed %v: %s.", issue.Id(), a.URL)
		a.CommentId = 0
		if a.URL != "" {
			activities = append(activities, a)
		}
	}
	return activities
}

// Issue handler ----------------------------------------------------------
type RietveldIssueHandler struct {
	SiteURL string
}

func NewRietveldIssueHandler(siteURL string) IssueHandler {
	handler := &RietveldIssueHandler{siteURL}
	return handler
}

func (handler *RietveldIssueHandler) LoadDetail(c appengine.Context, issueId string, detail* IssueDetail) error {
	var data RietveldIssueData
	requestURL := getRietveldJSONIssueURL(handler.SiteURL, issueId, false)
	err := loadJSONURL(c, requestURL, &data)
	if err != nil {
		return err
	}

	detail.IssueURL = "http://" + handler.SiteURL + "/" + issueId
	detail.FullDescription = data.Description
	detail.Cc = data.Cc
	detail.Reviewers = data.Reviewers
	detail.Files, err = loadRietveldIssueFiles(c, handler.SiteURL, issueId, &data)
	return err
}

func stringToDatastoreTime(ts string) (time.Time, error) {
	tm, err := time.Parse(rietveldTimeoutLayout, ts[:len(rietveldTimeoutLayout)])
	if err != nil {
		return time.Unix(0, 0), err
	}
	var microsecs time.Duration
	if len(ts) > len(rietveldTimeoutLayout) {
		subsecs := ts[len(rietveldTimeoutLayout)+1:]
		if i, err := strconv.Atoi(subsecs); err == nil {
			microsecs = time.Duration(i)
		}
	}
	return tm.Add(microsecs * time.Microsecond), nil
}

const (
	paramUnknown = "1"
	paramYes     = "2"
	paramNo      = "3"
)

var defaultSearchParams = map[string]string {
	"closed":        paramUnknown,
	"private":       paramUnknown, // This seems to be the only supported value.
	"commit":        paramUnknown,
	"with_messages": "False",
	"keys_only":     "True",
	"format":        "json",
}

func getRietveldMessageURL(issue *Issue, msg int) string {
	return fmt.Sprintf("http://%s/%v#msg%d", issue.SiteURL, issue.IssueId, msg)
}

func getRietveldCommitURL(issue *Issue) string {
	if m := rietveldCommitURLRegexp.FindStringSubmatch(issue.Description); m != nil {
		return m[1]
	}
	return ""
}
